/**
 * A standalone point geometry with useful accessor, comparison, and
 * modification methods.
 *
 * @class Point
 * @param {Number} x the x-coordinate. this could be longitude or screen
 * pixels, or any other sort of unit.
 * @param {Number} y the y-coordinate. this could be latitude or screen
 * pixels, or any other sort of unit.
 * @example
 * var point = new Point(-77, 38);
 */
function Point(x, y) {
	this.x = x;
	this.y = y;
}

Point.prototype = {

	/**
	 * Clone this point, returning a new point that can be modified
	 * without affecting the old one.
	 * @return {Point} the clone
	 */
	clone: function() { return new Point(this.x, this.y); },

	/**
	 * Add this point's x & y coordinates to another point,
	 * yielding a new point.
	 * @param {Point} p the other point
	 * @return {Point} output point
	 */
	add:     function(p) { return this.clone()._add(p); },

	/**
	 * Subtract this point's x & y coordinates to from point,
	 * yielding a new point.
	 * @param {Point} p the other point
	 * @return {Point} output point
	 */
	sub:     function(p) { return this.clone()._sub(p); },

	/**
	 * Multiply this point's x & y coordinates by point,
	 * yielding a new point.
	 * @param {Point} p the other point
	 * @return {Point} output point
	 */
	multByPoint:    function(p) { return this.clone()._multByPoint(p); },

	/**
	 * Divide this point's x & y coordinates by point,
	 * yielding a new point.
	 * @param {Point} p the other point
	 * @return {Point} output point
	 */
	divByPoint:     function(p) { return this.clone()._divByPoint(p); },

	/**
	 * Multiply this point's x & y coordinates by a factor,
	 * yielding a new point.
	 * @param {Point} k factor
	 * @return {Point} output point
	 */
	mult:    function(k) { return this.clone()._mult(k); },

	/**
	 * Divide this point's x & y coordinates by a factor,
	 * yielding a new point.
	 * @param {Point} k factor
	 * @return {Point} output point
	 */
	div:     function(k) { return this.clone()._div(k); },

	/**
	 * Rotate this point around the 0, 0 origin by an angle a,
	 * given in radians
	 * @param {Number} a angle to rotate around, in radians
	 * @return {Point} output point
	 */
	rotate:  function(a) { return this.clone()._rotate(a); },

	/**
	 * Rotate this point around p point by an angle a,
	 * given in radians
	 * @param {Number} a angle to rotate around, in radians
	 * @param {Point} p Point to rotate around
	 * @return {Point} output point
	 */
	rotateAround:  function(a,p) { return this.clone()._rotateAround(a,p); },

	/**
	 * Multiply this point by a 4x1 transformation matrix
	 * @param {Array<Number>} m transformation matrix
	 * @return {Point} output point
	 */
	matMult: function(m) { return this.clone()._matMult(m); },

	/**
	 * Calculate this point but as a unit vector from 0, 0, meaning
	 * that the distance from the resulting point to the 0, 0
	 * coordinate will be equal to 1 and the angle from the resulting
	 * point to the 0, 0 coordinate will be the same as before.
	 * @return {Point} unit vector point
	 */
	unit:    function() { return this.clone()._unit(); },

	/**
	 * Compute a perpendicular point, where the new y coordinate
	 * is the old x coordinate and the new x coordinate is the old y
	 * coordinate multiplied by -1
	 * @return {Point} perpendicular point
	 */
	perp:    function() { return this.clone()._perp(); },

	/**
	 * Return a version of this point with the x & y coordinates
	 * rounded to integers.
	 * @return {Point} rounded point
	 */
	round:   function() { return this.clone()._round(); },

	/**
	 * Return the magitude of this point: this is the Euclidean
	 * distance from the 0, 0 coordinate to this point's x and y
	 * coordinates.
	 * @return {Number} magnitude
	 */
	mag: function() {
		return Math.sqrt(this.x * this.x + this.y * this.y);
	},

	/**
	 * Judge whether this point is equal to another point, returning
	 * true or false.
	 * @param {Point} other the other point
	 * @return {boolean} whether the points are equal
	 */
	equals: function(other) {
		return this.x === other.x &&
			this.y === other.y;
	},

	/**
	 * Calculate the distance from this point to another point
	 * @param {Point} p the other point
	 * @return {Number} distance
	 */
	dist: function(p) {
		return Math.sqrt(this.distSqr(p));
	},

	/**
	 * Calculate the distance from this point to another point,
	 * without the square root step. Useful if you're comparing
	 * relative distances.
	 * @param {Point} p the other point
	 * @return {Number} distance
	 */
	distSqr: function(p) {
		var dx = p.x - this.x,
			dy = p.y - this.y;
		return dx * dx + dy * dy;
	},

	/**
	 * Get the angle from the 0, 0 coordinate to this point, in radians
	 * coordinates.
	 * @return {Number} angle
	 */
	angle: function() {
		return Math.atan2(this.y, this.x);
	},

	/**
	 * Get the angle from this point to another point, in radians
	 * @param {Point} b the other point
	 * @return {Number} angle
	 */
	angleTo: function(b) {
		return Math.atan2(this.y - b.y, this.x - b.x);
	},

	/**
	 * Get the angle between this point and another point, in radians
	 * @param {Point} b the other point
	 * @return {Number} angle
	 */
	angleWith: function(b) {
		return this.angleWithSep(b.x, b.y);
	},

	/*
	 * Find the angle of the two vectors, solving the formula for
	 * the cross product a x b = |a||b|sin(θ) for θ.
	 * @param {Number} x the x-coordinate
	 * @param {Number} y the y-coordinate
	 * @return {Number} the angle in radians
	 */
	angleWithSep: function(x, y) {
		return Math.atan2(
			this.x * y - this.y * x,
			this.x * x + this.y * y);
	},

	_matMult: function(m) {
		var x = m[0] * this.x + m[1] * this.y,
			y = m[2] * this.x + m[3] * this.y;
		this.x = x;
		this.y = y;
		return this;
	},

	_add: function(p) {
		this.x += p.x;
		this.y += p.y;
		return this;
	},

	_sub: function(p) {
		this.x -= p.x;
		this.y -= p.y;
		return this;
	},

	_mult: function(k) {
		this.x *= k;
		this.y *= k;
		return this;
	},

	_div: function(k) {
		this.x /= k;
		this.y /= k;
		return this;
	},

	_multByPoint: function(p) {
		this.x *= p.x;
		this.y *= p.y;
		return this;
	},

	_divByPoint: function(p) {
		this.x /= p.x;
		this.y /= p.y;
		return this;
	},

	_unit: function() {
		this._div(this.mag());
		return this;
	},

	_perp: function() {
		var y = this.y;
		this.y = this.x;
		this.x = -y;
		return this;
	},

	_rotate: function(angle) {
		var cos = Math.cos(angle),
			sin = Math.sin(angle),
			x = cos * this.x - sin * this.y,
			y = sin * this.x + cos * this.y;
		this.x = x;
		this.y = y;
		return this;
	},

	_rotateAround: function(angle, p) {
		var cos = Math.cos(angle),
			sin = Math.sin(angle),
			x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),
			y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y);
		this.x = x;
		this.y = y;
		return this;
	},

	_round: function() {
		this.x = Math.round(this.x);
		this.y = Math.round(this.y);
		return this;
	}
};

/**
 * Construct a point from an array if necessary, otherwise if the input
 * is already a Point, or an unknown type, return it unchanged
 * @param {Array<Number>|Point|*} a any kind of input value
 * @return {Point} constructed point, or passed-through value.
 * @example
 * // this
 * var point = Point.convert([0, 1]);
 * // is equivalent to
 * var point = new Point(0, 1);
 */
Point.convert = function (a) {
	if (a instanceof Point) {
		return a;
	}
	if (Array.isArray(a)) {
		return new Point(a[0], a[1]);
	}
	return a;
};